﻿namespace Ngs.Data.Objects
{
    #region Using Directives

    using System;
    using System.Collections.Generic;
    using System.Data.Objects;
    using System.Linq;
    using System.Linq.Expressions;
    using Ngs.Collections;
    using Ngs.Expressions;
    #endregion

    public class QueryProxy<T, K> : DataQuery<T>
    {
        private Dictionary<Type, IQueryable> typesSupported;
        private IQueryable<K> query;

        #region Constructors

        public QueryProxy(IQueryable<K> query)
            : this(query, null)
        { }

        public QueryProxy(IQueryable<K> query, Converter<K, T> converter)
            : this(query, converter, null)
        {
        }

        public QueryProxy(IQueryable<K> query, Converter<K, T> converter, IEnumerable<IQueryable> subQueryables)
        {
            ExceptionFactory.ThrowIfArgumentNull(query);

            this.query = query;
            this.Converter = converter;

            if (subQueryables != null)
            {
                this.typesSupported = new Dictionary<Type, IQueryable>();
                foreach (IQueryable item in subQueryables)
                {
                    this.typesSupported.Add(item.ElementType, item);
                }
            }
        }
        #endregion

        #region Properties

        public Converter<K, T> Converter { get; set; }

        public override Expression Expression
        {
            get { return this.Query.Expression; }
        }

        public IQueryable<K> Query { get { return this.query; } }

        #endregion

        #region Public Methods

        public override IEnumerator<T> GetEnumerator()
        {
            IEnumerator<K> resultEnum = this.query.GetEnumerator();
            if (resultEnum != null)
            {
                while (resultEnum.MoveNext())
                {
                    yield return this.Converter(resultEnum.Current);
                }
            }
        }

        #region Linq Operators

        public override T First()
        {
            IEnumerator<T> enumerator = this.GetEnumerator();
            enumerator.MoveNext();
            return enumerator.Current;
        }

        public override T First(Expression<Func<T, bool>> predicate)
        {
            Expression<Func<K, bool>> body = (Expression<Func<K, bool>>)Replace(predicate);
            return this.Converter(this.query.First(body));
        }

        public override IDataQuery<IGrouping<TKey, T>> GroupBy<TKey>(Expression<Func<T, TKey>> keySelector)
        {
            Expression<Func<K, TKey>> body = (Expression<Func<K, TKey>>)Replace(keySelector);
            IQueryable<IGrouping<TKey, K>> groupBy = this.query.GroupBy(body);

            Converter<IGrouping<TKey, K>, IGrouping<TKey, T>> converter = new Converter<IGrouping<TKey, K>, IGrouping<TKey, T>>(GroupByItem);

            QueryProxy<IGrouping<TKey, T>, IGrouping<TKey, K>> group = new QueryProxy<IGrouping<TKey, T>, IGrouping<TKey, K>>(groupBy, converter);
            return group;
        }

        public override IDataQuery<TKey> OfType<TKey>()
        {
            // Unable to cast generic types therefore should use alternative method.
            ExceptionFactory.ThrowArgumentException(typeof(TKey) == typeof(T), "Use parameterless OfType for same type filtering.");

            ExceptionFactory.ThrowInvalidOperation(this.typesSupported == null || this.typesSupported.Count == 0, "No other sub types configured for querying.");

            IQueryable queryable = null;
            ExceptionFactory.ThrowInvalidOperation(!typesSupported.TryGetValue(typeof(TKey), out queryable));

            return ((DataQuery<TKey>)queryable).OfType();
        }

        public override IDataQuery<T> OfType()
        {
            this.query = this.query.OfType<K>();
            return base.OfType();
        }

        public override IDataQuery<T> OrderBy<TKey>(Expression<Func<T, TKey>> keySelector)
        {
            Expression<Func<K, TKey>> body = (Expression<Func<K, TKey>>)Replace(keySelector);

            this.query = this.query.OrderBy(body);
            return this;
        }

        public override IDataQuery<T> OrderByDescending<TKey>(Expression<Func<T, TKey>> keySelector)
        {
            Expression<Func<K, TKey>> body = (Expression<Func<K, TKey>>)Replace(keySelector);

            this.query = this.query.OrderByDescending(body);
            return this;
        }

        public override IDataQuery<T> Reverse()
        {
            this.query = this.query.Reverse();
            return this;
        }

        public override IDataQuery<T> Skip(int count)
        {
            this.query = query.Skip(count);
            return this;
        }

        public override IDataQuery<T> Take(int count)
        {
            this.query = this.query.Take(count);
            return this;
        }

        public override IDataQuery<T> Where(Expression<Func<T, bool>> predicate)
        {
            Expression<Func<K, bool>> body = (Expression<Func<K, bool>>)Replace(predicate);

            this.query = this.query.Where(body);
            return this;
        }

        public override int Count()
        {
            return this.query.Count<K>();
        }

        #endregion

        #endregion

        #region Private/Protected/Internal Methods

        protected override void Include(string subObject)
        {
            ObjectQuery<K> efQuery = query as ObjectQuery<K>;
            ExceptionFactory.ThrowInvalidOperation(efQuery == null, "IQueryProvider does not support 'Include' semantics.");
            efQuery.Include(subObject);
            base.Include(subObject);
        }

        private IGrouping<TKey, T> GroupByItem<TKey>(IGrouping<TKey, K> groupBy)
        {
            QueryProxy<T, K> elements = new QueryProxy<T, K>(groupBy.AsQueryable<K>(), this.Converter);
            Grouping<TKey, T> item = new Grouping<TKey, T>(groupBy.Key, elements);

            return item;
        }

        private Expression Replace(LambdaExpression lambda)
        {
            return ExpressionExtensions.Replace(typeof(K), lambda);
        }
        #endregion
    }
}

